#!/usr/bin/env perl
#
#
# 2007 Tod E. Kurt, tod@thingm.com, ThingM
#

# command sent
# header,reserved,length,command,data,cksum 
#"\xff\x00\xNN\xCC\xDD\xCS";
#
# response recvd
# header,reserved,length,command,response(N),cksum
# chksum is adding all bytes except the header

use strict;
use warnings;


use Time::HiRes qw(sleep);
use Device::SerialPort;
use Getopt::Long;

my $port;       # serial port, like "/dev/tty.usbserial"
my $mode;       # operating mode: scan, write, movie
my $block;      # block on RFID card to read or write, usually '1'
my $value;

my $DEBUG = 0;

my $me = "sonmicro-mifare.pl";
sub usage() {
    print <<EOF;
Usage: $me <mode> -p <serial_port> [options]

Where operating <mode> is one of: -scan, -movie, -write, -read

Options:
  -p|port <serial_port>  Serial port of RFID reader (e.g. "/dev/tty.usbserial")
  -b|block <block_num>   RFID tag block to read/write (e.g. "1")
  -v|val <value>         Value to write, if in write mode. 4-bytes
  -debug                 Turn on debugging output

EOF
exit(1);
}


###########################################################################
#
# RFID reader functions
#
sub rfid_open($) {
    my ($port) = @_;
    my $ob = Device::SerialPort->new($port) 
        || die "Can't Open $port: $!";
    $ob->baudrate(19200)   || die "failed setting baudrate";
    $ob->parity("none")    || die "failed setting parity";
    $ob->databits(8)       || die "failed setting databits";
    $ob->handshake("none") || die "failed setting handshake";
    $ob->read_const_time(200) || die "failed setting read timeout";
    $ob->read_char_time(20) || die "failed setting read timeout"; 
    $ob->write_settings    || die "no settings";
    return $ob;
}

sub rfid_halt($) {
    my ($ob) = @_;
    my $cmd = pack("C*", 0xff,0x01,0x93,0x94);  # halt  (no response)
    #printf "writing %s ...\n", to_hex($cmd);
    my $pass = $ob->write($cmd);
}

sub rfid_send_reset($) {
    my ($ob) = @_;
    my $cmd = pack("C*", 0xff,0x00,0x01,0x80,0x81);  # reset
    #printf "writing %s ...\n", to_hex($cmd);
    my $pass = $ob->write($cmd);
    sleep(0.2); # wait for the reset
    my $bytecnt = 11;      # firmware version response is 11 bytes
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    #print "rfid_send_reset: error\n" if( $count_in != $bytecnt );
}

sub rfid_send_seektag($) {
    my ($ob) = @_;
    my $cmd = pack("C*", 0xff,0x00,0x01,0x82,0x83); # seek for tag
#    printf "writing %s ...\n", to_hex($cmd);
    my $pass = $ob->write($cmd);
    # should get back 0xff,0x00,0x02,0x82,0x4c,0xd0);
    # 4c == command in progress
    my $bytecnt = 6;
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    if( defined $count_in && $count_in != $bytecnt ) {
        print "rfid_send_seektag: error\n" 
    }
}
sub rfid_read_seektag($) {
    my ($ob) = @_;
    # now read actual tag id
    my $bytecnt = 10;
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    if( defined $count_in &&  $count_in == $bytecnt ) {
        return $string_in;
    }
    return '';
}

# returns the tag id number & type, or nothing if no tag
sub rfid_select_tag($) {
    my ($ob) = @_;
    my $cmd = pack("C*", 0xff,0x00,0x01,0x83,0x84); # select tag
    my $pass = $ob->write($cmd);
    my $bytecnt = 10; # numbytes when tag found & selected
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    if( defined $count_in && $count_in == $bytecnt ) {
        return $string_in;
    }
    return '';
}

sub rfid_authenticate_simple($$) {
    my ($ob,$blk) = @_;
    my @cmd = (0xff,0x00,0x03,0x85,$blk,0xff,0x88,0x00); # auth blk
    rfid_set_chksum(\@cmd);
    my $pass = $ob->write( pack("C*", @cmd) );
    my $bytecnt = 6;
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    if( defined $count_in &&  $count_in == $bytecnt ) {
        return $string_in;
    }
    return '';
}

sub rfid_read_value_block($$) {
    my ($ob,$blk) = @_;
    my @cmd = (0xff,0x00,0x02,0x87,$blk,0x00); # read val blk
    rfid_set_chksum(\@cmd);
    my $pass = $ob->write( pack("C*",@cmd) );
    my $bytecnt = 10;
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    if( defined $count_in &&  $count_in == $bytecnt ) {
        return $string_in;
    }
    return '';
}

# write val blk cmd = 0x8a
sub rfid_write_value_block($$$) {
    my ($ob,$blk,$val) = @_;
    my @v = rfid_val_to_bytearray($val);
    my @cmd = (0xff,0x00,0x06,0x8a, $blk, $v[0],$v[1],$v[2],$v[3], 0x00);
    rfid_set_chksum(\@cmd);
    my $cmd = pack("C*", @cmd);
    my $pass = $ob->write($cmd);
    my $bytecnt = 10;
    my ($count_in, $string_in) = $ob->read($bytecnt);
    print "wvb count_in:$count_in, string_in:".to_hex($string_in)."\n" if($DEBUG);
    if( defined $count_in &&  $count_in == $bytecnt ) {
        return $string_in;
    }
    return '';
}

sub rfid_outputs_on($) {
    my ($ob) = @_;
    my $cmd = pack("C*", 0xff,0x00,0x02,0x92,0x03,0x97);  # turn on both
    my $pass = $ob->write($cmd);
    my $bytecnt = 6;
    my ($count_in, $string_in) = $ob->read($bytecnt);
}

sub rfid_outputs_off($) {
    my ($ob) = @_;
    my $cmd = pack("C*", 0xff,0x00,0x02,0x92,0x00,0x94);  # turn off both
    my $pass = $ob->write($cmd);
    my $bytecnt = 6;
    my ($count_in, $string_in) = $ob->read($bytecnt);
}

# utility method pulls out tag id from typical response bytearray
sub rfid_response_to_tag($) {
    my ($bytes) = @_;
    my @rbytes = reverse unpack("C*",$bytes);
    return join('', map {sprintf "%02X",$_} @rbytes[1..4]);
}
# for parsing value blocks
sub rfid_response_to_value($) {
    my ($bytes) = @_;
    my @rbytes = reverse unpack("C*",$bytes);
    return join('', map {sprintf "%02X",$_} @rbytes[1..4]);
}

# utility method to compute checksum at set last byte of passed in array
sub rfid_set_chksum($) {
    my ($cmd) = @_;     # notice, it takes a refarray, not an array
    my $last = $#$cmd;  # this is ugly, but gets the index of the last element
    my $chksum=0; 
    map { $chksum += $_; } @{$cmd}[1..$last];
    $cmd->[-1] = $chksum & 0xff;
}

# note: this pack("L") may fail on non-intel machines due to endian-ness
# perhaps  a better way would be to use bitmasks and shifts to get at the bytes
sub rfid_val_to_bytearray($) {
    my ($val) = @_;
    return unpack("C*", pack("L",$val));  # convert to byte array
}
# unfinished
sub rfid_bytearray_to_val($) {
    my ($barr) = @_;
    #my @rbytes = reverse unpack("C*",$bytes);
}
sub to_hex($;$) {
    my ($x,$sep) = @_;
    $sep = " " if(!defined $sep);
    return join ($sep, map { sprintf "%02x", $_ } unpack("C*", $x)),
}


###########################################################################
#
#  Begin the actual code
#

my $help = 0;
my $rc = GetOptions( 'port|p=s'          => \$port,
                     'scan'              => sub { $mode ='scan' },
                     'read'              => sub { $mode ='read'  },
                     'write'             => sub { $mode ='write' },
                     'block|b=i'         => \$block,
                     'value|val|v=o'     => \$value,
                     'help'              => \$help,
                     'debug'             => \$DEBUG,
                     );

usage() if( $rc==0 or @ARGV>0 or $help );
if( !defined $port ) {
    die "Error: must supply a serial port!\n";
}
if( !defined $mode) {
    die "Error: must give a mode!\n";
}

if( $mode eq 'scan') { 
    print "Scan mode\n";
} elsif( $mode eq 'test') {
    print "Test mode\n";
} elsif( $mode eq 'write') {
    die "Error: must specify block to write\n" if( !defined $block );
    die "Error: must specify value to write\n" if( !defined $value );
    printf "Write mode: will write block $block with value %#02x\n",$value;
} elsif( $mode eq 'read') {
    die "Error: must specify block to read\n" if( !defined $block );
    print "Read mode: will read block $block\n";
} else {
    die "Error: unknown mode: $mode\n";
}

START:

print "Opening reader '$port'.\n";

my $r = rfid_open( $port );

rfid_halt($r);
rfid_send_reset($r);
rfid_send_reset($r);  # just in case
rfid_outputs_off($r);
sleep(0.5);
rfid_outputs_on($r);
sleep(1);
rfid_outputs_off($r);

$|=1; # unbuffer stdout

my $resp;
my $bad=0;
if( $mode eq 'scan' ) {
    print "looking for tags (press ctrl-c to stop)...";
    while( 1 ) {
        print ".";
        $resp = rfid_select_tag($r);
        next if( $resp eq '' );
        rfid_outputs_on($r);
        my $tag = rfid_response_to_tag($resp);
        print "\nresp: ", to_hex($resp), "\n" if($DEBUG);
        print "\ntag: $tag\n";
        sleep(0.5);
        rfid_outputs_off($r);
    }
}
elsif( $mode eq 'write' ) {
    print "looking for tags (press ctrl-c to stop)...";
    while(1) { 
        print ".";
        $resp = rfid_select_tag($r);
        next if( $resp eq '' ); # no tag
        print "\n";
        $resp = rfid_authenticate_simple($r,$block);
        if( $resp eq '' ) {
            print "error authenticating.\n";
            next;
        }
        $resp = rfid_write_value_block($r, $block, $value);
        if ($resp eq '') {
            print "error writing value block.\n";
            next;
        } 
        printf "write OK for block $block with value %#02x\n",$value;
        sleep 2;
    }
}
elsif( $mode eq 'read' ) {
    print "looking for tags (press ctrl-c to stop)...";
    while(1) {
        if( $bad > 20 ) { goto RESTART; }
        print ".";
        $resp = rfid_select_tag($r);
        next if( $resp eq '' );  # no tag
        print "\n";
        rfid_outputs_on($r);
        $resp = rfid_authenticate_simple($r,$block);
        if( $resp eq '' ) {
            print "error authenticating.\n";
            $bad++;
            next;
        }
        $resp = rfid_read_value_block($r,$block);
        if ($resp eq '') {
            print "no value bock or error reading value block.\n";
            next;
        }
        my $v = rfid_response_to_value($resp);
        print "read value $v from block $block\n";
        $bad-- if($bad>0);
        sleep 1;
        rfid_outputs_off($r);
    }
}

RESTART:
    if( $r ) { $r->close || warn "close failed" }
goto START;
